using System;
using System.IO;
using System.Reflection;
using System.ComponentModel;
using System.Globalization;
using System.Security.Permissions;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
#if !PocketPC || DesignTime
using System.Runtime.Serialization;
using System.Windows.Forms;
using System.ComponentModel.Design.Serialization;
#endif

namespace GeoFramework
{
    /// <summary>
    /// Represents an Earth-centered, Earth-fixed (ECEF) Cartesian coordinate.
    /// </summary>
#if !PocketPC || DesignTime
#if Framework20
    [TypeConverter("GeoFramework.Design.CartesianConverter, GeoFramework.Design, Culture=neutral, Version=1.0.0.0, PublicKeyToken=3ed3cdf4fdda3400")]
#elif Framework20
    [TypeConverter("GeoFramework.Design.CartesianConverter, GeoFramework.Design, Culture=neutral, Version=1.0.0.0, PublicKeyToken=3ed3cdf4fdda3400")]
#else
	[TypeConverter(typeof(Design.CartesianPointConverter))]
#endif
    [Serializable()]
    [ImmutableObject(true)]
#endif
#if Framework20
    [XmlSchemaProvider("GetXmlSchema")]
#endif
    public class CartesianPoint : IFormattable 
#if !PocketPC || Framework20
	,IXmlSerializable
#endif
#if !PocketPC || DesignTime
	, ISerializable
#endif
    {
        #region Private Variables

        private Distance pX;
        private Distance pY;
        private Distance pZ;

        #endregion

        #region Static Members

        public static readonly CartesianPoint Empty = new CartesianPoint(Distance.Empty, Distance.Empty, Distance.Empty);
        public static readonly CartesianPoint Infinity = new CartesianPoint(Distance.Infinity, Distance.Infinity, Distance.Infinity);

        #endregion

        #region Constructors

        /// <summary>
        /// Creates a new instance with empty values.
        /// </summary>
        public CartesianPoint()
        {
            pX = Distance.Empty;
            pY = Distance.Empty;
            pZ = Distance.Empty;
        }

        /// <summary>
        /// Creates a new instance using the specified X, Y and Z values.
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <param name="z"></param>
        public CartesianPoint(Distance x, Distance y, Distance z)
        {
            pX = x.ToMeters();
            pY = y.ToMeters();
            pZ = z.ToMeters();
        }

#if !PocketPC || DesignTime
        /// <summary>
        /// Creates a new instance by de-serializing previously-serialized information.
        /// </summary>
        /// <param name="info">A <strong>SerializationInfo</strong> object containing serialized data.</param>
        /// <param name="context">A <strong>StreamingContext</strong> object describing the context of serialization.</param>
        /// <remarks>This constructor will de-serialize a previously-serialized object.  Objects are created
        /// serialized and de-serialized based on the "DecimalDegrees" value to maintain precision.</remarks>
        private CartesianPoint(SerializationInfo info, StreamingContext context)
        {
            pX = new Distance(info.GetDouble("X"), DistanceUnit.Meters);
            pY = new Distance(info.GetDouble("Y"), DistanceUnit.Meters);
            pZ = new Distance(info.GetDouble("Z"), DistanceUnit.Meters);
        }
#endif

        #endregion

        #region Public Properties

        /// <summary>
        /// Returns the horizontal (longitude) portion of a Cartesian coordinate.
        /// </summary>
        [XmlElement("X")]
        public Distance X
        {
            get
            {
                return pX;
            }
        }

        /// <summary>
        /// Returns the vertical (latitude) portion of a Cartesian coordinate.
        /// </summary>
        [XmlElement("Y")]
        public Distance Y
        {
            get
            {
                return pY;
            }
        }

        /// <summary>
        /// Returns the altitude portion of a Cartesian coordinate.
        /// </summary>
        [XmlElement("Z")]
        public Distance Z
        {
            get
            {
                return pZ;
            }
        }

        #endregion

        /// <summary>
        /// Converts the current instance to a geodetic (latitude/longitude) coordinate.
        /// </summary>
        /// <returns>A <strong>Position</strong> object containing the converted result.</returns>
        /// <remarks>The conversion formula will convert the Cartesian coordinate to
        /// latitude and longitude using the WGS1984 ellipsoid (the default ellipsoid for
        /// GPS coordinates).</remarks>
        public Position3D ToPosition3D()
        {
            return ToPosition3D(Ellipsoid.Wgs1984);
        }

        /// <summary>
        /// Converts the current instance to a geodetic (latitude/longitude) coordinate using the specified ellipsoid.
        /// </summary>
        /// <returns>A <strong>Position</strong> object containing the converted result.</returns>
        /// <remarks>The conversion formula will convert the Cartesian coordinate to
        /// latitude and longitude using the WGS1984 ellipsoid (the default ellipsoid for
        /// GPS coordinates).  The resulting three-dimensional coordinate is accurate to within two millimeters
        /// (2 mm).</remarks>
        public Position3D ToPosition3D(Ellipsoid ellipsoid)
        {
            if (ellipsoid == null)
                throw new ArgumentNullException("ellipsoid");

            #region New code

            /*
             * % ECEF2LLA - convert earth-centered earth-fixed (ECEF)
%            cartesian coordinates to latitude, longitude,
%            and altitude
%
% USAGE:
% [lat,lon,alt] = ecef2lla(x,y,z)
%
% lat = geodetic latitude (radians)
% lon = longitude (radians)
% alt = height above WGS84 ellipsoid (m)
% x = ECEF X-coordinate (m)
% y = ECEF Y-coordinate (m)
% z = ECEF Z-coordinate (m)
%
% Notes: (1) This function assumes the WGS84 model.
%        (2) Latitude is customary geodetic (not geocentric).
%        (3) Inputs may be scalars, vectors, or matrices of the same
%            size and shape. Outputs will have that same size and shape.
%        (4) Tested but no warranty; use at your own risk.
%        (5) Michael Kleder, April 2006

function [lat,lon,alt] = ecef2lla(x,y,z)

% WGS84 ellipsoid constants:
a = 6378137;
e = 8.1819190842622e-2;

% calculations:
b   = sqrt(a^2*(1-e^2));
ep  = sqrt((a^2-b^2)/b^2);
p   = sqrt(x.^2+y.^2);
th  = atan2(a*z,b*p);
lon = atan2(y,x);
lat = atan2((z+ep^2.*b.*sin(th).^3),(p-e^2.*a.*cos(th).^3));
N   = a./sqrt(1-e^2.*sin(lat).^2);
alt = p./cos(lat)-N;

% return lon in range [0,2*pi)
lon = mod(lon,2*pi);

% correct for numerical instability in altitude near exact poles:
% (after this correction, error is about 2 millimeters, which is about
% the same as the numerical precision of the overall function)

k=abs(x)<1 & abs(y)<1;
alt(k) = abs(z(k))-b;

return
             */

            double x = pX.ToMeters().Value;
            double y = pY.ToMeters().Value;
            double z = pZ.ToMeters().Value;

            //% WGS84 ellipsoid constants:
            //a = 6378137;

            double a = ellipsoid.EquatorialRadius.ToMeters().Value;

            //e = 8.1819190842622e-2;

            double e = ellipsoid.Eccentricity;

            //% calculations:
            //b   = sqrt(a^2*(1-e^2));

            double b = Math.Sqrt(Math.Pow(a, 2) * (1 - Math.Pow(e, 2)));

            //ep  = sqrt((a^2-b^2)/b^2);

            double ep = Math.Sqrt((Math.Pow(a, 2) - Math.Pow(b, 2)) / Math.Pow(b, 2));

            //p   = sqrt(x.^2+y.^2);

            double p = Math.Sqrt(Math.Pow(x, 2) + Math.Pow(y, 2));

            //th  = atan2(a*z,b*p);

            double th = Math.Atan2(a * z, b * p);

            //lon = atan2(y,x);

            double lon = Math.Atan2(y, x);

            //lat = atan2((z+ep^2.*b.*sin(th).^3),(p-e^2.*a.*cos(th).^3));

            double lat = Math.Atan2((z + Math.Pow(ep, 2) * b * Math.Pow(Math.Sin(th), 3)), (p - Math.Pow(e, 2) * a * Math.Pow(Math.Cos(th), 3)));

            //N   = a./sqrt(1-e^2.*sin(lat).^2);

            double N = a / Math.Sqrt(1 - Math.Pow(e, 2) * Math.Pow(Math.Sin(lat), 2));

            //alt = p./cos(lat)-N;

            double alt = p / Math.Cos(lat) - N;

            //% return lon in range [0,2*pi)
            //lon = mod(lon,2*pi);

            lon = lon % (2 * Math.PI);

            //% correct for numerical instability in altitude near exact poles:
            //% (after this correction, error is about 2 millimeters, which is about
            //% the same as the numerical precision of the overall function)

            //k=abs(x)<1 & abs(y)<1;

            bool k = Math.Abs(x) < 1.0 && Math.Abs(y) < 1.0;

            //alt(k) = abs(z(k))-b;

            if (k)
                alt = Math.Abs(z) - b;

            //return

            return new Position3D(Latitude.FromRadians(lat), Longitude.FromRadians(lon), Distance.FromMeters(alt));

            #endregion
        }

        /// <summary>
        /// Returns the distance from the current instance to the specified cartesian point.
        /// </summary>
        /// <param name="point">A <strong>CartesianPoint</strong> object representing the end of a segment.</param>
        /// <returns></returns>
        public Distance DistanceTo(CartesianPoint point)
        {
            return new Distance(
                Math.Sqrt(Math.Pow(point.X.Value - pX.Value, 2) 
                        + Math.Pow(point.Y.Value - pY.Value, 2))
                , DistanceUnit.Meters).ToLocalUnitType();
        }

        /// <summary>
        /// Converts the current instance to a geodetic (latitude/longitude) coordinate.
        /// </summary>
        /// <returns>A <strong>Position</strong> object containing the converted result.</returns>
        /// <remarks>The conversion formula will convert the Cartesian coordinate to
        /// latitude and longitude using the WGS1984 ellipsoid (the default ellipsoid for
        /// GPS coordinates).</remarks>
        public Position ToPosition()
        {
            return ToPosition3D(Ellipsoid.Wgs1984) as Position;
        }

        /// <summary>
        /// Converts the current instance to a geodetic (latitude/longitude) coordinate using the specified ellipsoid.
        /// </summary>
        /// <returns>A <strong>Position</strong> object containing the converted result.</returns>
        /// <remarks>The conversion formula will convert the Cartesian coordinate to
        /// latitude and longitude using the WGS1984 ellipsoid (the default ellipsoid for
        /// GPS coordinates).</remarks>
        public Position ToPosition(Ellipsoid ellipsoid)
        {
            return ToPosition3D(ellipsoid) as Position;
        }

        public override string ToString()
        {
            return ToString("v u", CultureInfo.CurrentCulture);
        }

        public virtual string ToString(string format)
        {
            return ToString(format, CultureInfo.CurrentCulture);
        }

        public virtual string ToString(string format, CultureInfo culture)
        {
            return pX.ToString(format, culture) + culture.TextInfo.ListSeparator
                + pY.ToString(format, culture) + culture.TextInfo.ListSeparator
                + pZ.ToString(format, culture);
        }

#if !PocketPC || (PocketPC && Framework20)
		#region IXmlSerializable Members

#if Framework20
        public static XmlQualifiedName GetXmlSchema(XmlSchemaSet xs)
        {
            // Load it from a string
            xs.Add(Xml.SchemaSet);

            // And return the type
            return new XmlQualifiedName("CartesianPoint", Xml.Namespace);
        }
#endif      

        XmlSchema IXmlSerializable.GetSchema()
        {
            return null;
        }

        void IXmlSerializable.ReadXml(XmlReader reader)
        {
            ReadXml(reader);
        }

        void IXmlSerializable.WriteXml(XmlWriter writer)
        {
            WriteXml(writer);
        }

        /// <summary>
        /// Populates the current instance by reading XML from the specified reader.
        /// </summary>
        /// <param name="reader"></param>
        public void ReadXml(XmlReader reader)
        {
            if (reader == null)
                throw new ArgumentNullException("reader");

            // Defer to the Latitude and Longitude serializers for other stuff
            XmlSerializer DistanceSerializer = new XmlSerializer(typeof(Distance));
           
            // Rember which level we are processing at.
            int InitialDepth = reader.Depth;

            // Read into deeper content
            reader.Read();

            // Read to the next item, or exit if any content is missing
            while(reader.Depth >= InitialDepth)
            {
                // Keep reading until we reach an element
                switch (reader.NodeType)
                {
                    case XmlNodeType.Element:
                        switch (reader.Name)
                        {
                            case "X":
                                reader.Read();
                                pX = (Distance)DistanceSerializer.Deserialize(reader);
                                reader.Read();
                                break;
                            case "Y":
                                reader.Read();
                                pY = (Distance)DistanceSerializer.Deserialize(reader);
                                reader.Read();
                                break;
                            case "Z":
                                reader.Read();
                                pZ = (Distance)DistanceSerializer.Deserialize(reader);
                                reader.Read();
                                break;
                            default:
                                // Read the next item
                                reader.Read();
                                break;
                        }
                        break;
                    case XmlNodeType.EndElement:
                        // Read the end element and exit
                        reader.Read();
                        return;
                    default:
                        // Read the next item
                        reader.Read();
                        break;
                }
            }
        }

        /// <summary>
        /// Writes the values of the current instance to the specified XML writer.
        /// </summary>
        /// <param name="writer"></param>
        public void WriteXml(XmlWriter writer)
        {
            if (writer == null)
                throw new ArgumentNullException("writer");

            XmlSerializer s = new XmlSerializer(typeof(Distance));
            writer.WriteStartElement("X");
            s.Serialize(writer, pX);
            writer.WriteEndElement();

            writer.WriteStartElement("Y");
            s.Serialize(writer, pY);
            writer.WriteEndElement();

            writer.WriteStartElement("Z");
            s.Serialize(writer, pZ);
            writer.WriteEndElement();
        }

		#endregion
#endif

#if !PocketPC || DesignTime
        #region ISerializable Members

        [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)]  
        void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
        {
            info.AddValue("X", pX.ToMeters().Value);
            info.AddValue("Y", pY.ToMeters().Value);
            info.AddValue("Z", pZ.ToMeters().Value);
        }

        #endregion
#endif

        #region IFormattable Members

        string IFormattable.ToString(string format, IFormatProvider formatProvider)
        {
            return ToString(format, (CultureInfo)formatProvider);
        }

        #endregion
    }
}
